Nuxt3 提供了強大而靈活的路由系統,結合 TypeScript 的靜態類型檢查,可以大大提高開發效率和代碼質量。本文將深入探討 Nuxt3 中的路由管理和中間件(Middleware)的使用,並結合先前學習的概念,如 Pinia、Zod、Vee-Validate 等,構建一個健壯的路由系統。我們還將討論 Nuxt3 的生命週期、cookies 的應用,以及如何處理客戶端存儲和 hydration 問題。
Nuxt3 使用基於文件系統的路由,這意味著你在 pages
目錄中創建的 Vue 組件會自動映射到相應的路由。
在 pages
目錄下創建以下文件結構:
pages/
index.vue
about.vue
users/
[id].vue
或是使用指令創建
bunx nuxi add page index
bunx nuxi add page about
bunx nuxi add page users/[id]
這將自動生成以下路由:
/
: 對應 index.vue
/about
: 對應 about.vue
/users/:id
: 對應 users/[id].vue
Nuxt3 支持兩種類型的中間件:全局中間件和路由中間件。
在 server/middleware
目錄下創建 auth.ts
:
import { defineEventHandler, parseCookies } from 'h3'
import * as zod from 'zod'
const userSchema = zod.object({
id: zod.number(),
role: zod.enum(['user', 'admin'])
})
export default defineEventHandler((event) => {
const cookies = parseCookies(event)
const userCookie = cookies.user
if (!userCookie) {
return
}
try {
const user = userSchema.parse(JSON.parse(userCookie))
event.context.user = user
} catch (error) {
console.error('Invalid user data in cookie')
}
})
在 middleware
目錄下創建 auth.ts
:
import { useUserStore } from '~/stores/userStore'
import { storeToRefs } from 'pinia'
export default defineNuxtRouteMiddleware((to, from) => {
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
if (!user.value && to.path !== '/login') {
return navigateTo('/login')
}
})
在 pages
目錄下的 Vue 組件中使用中間件:
<script setup lang="ts">
definePageMeta({
middleware: ['auth']
})
</script>
<template>
<div>
<h1>Protected Page</h1>
</div>
</template>
在 stores
目錄下創建 userStore.ts
:
import { defineStore } from 'pinia'
import * as zod from 'zod'
const userSchema = z.object({
id: zod.number(),
name: zod.string(),
email: zod.string().email(),
role: zod.enum(['user', 'admin'])
})
type User = zod.infer<typeof userSchema>
export const useUserStore = defineStore('user', () => {
const user = ref<UserSchema | null>(null);
// methods::
const fetchUser = async (): Promise<void> => {
const response = await $fetch('/api/user', {
method: 'GET',
});
const validator = userSchema.safeParse(response);
if (!validator.success) {
throw new TypeError('validator error');
}
user.value = validator.data;
};
const logout = (): void => {
user.value = null;
};
return {
// state::
user,
// methods::
fetchUser,
logout,
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot));
}
在 composables
目錄下創建 useAuth.ts
:
import { useUserStore } from '~/stores/userStore'
import { useLocalStorage } from '@vueuse/core'
export function useAuth() {
const userStore = useUserStore()
const isAuthenticated = computed(() => !!userStore.user)
const login = async (email: string, password: string) => {
// 在實際應用中,這裡應該調用登錄 API
await userStore.fetchUser()
if (process.client) {
useLocalStorage('auth_token', 'fake_token')
}
}
const logout = () => {
userStore.logout()
if (process.client) {
useLocalStorage('auth_token', null)
}
}
return {
isAuthenticated,
login,
logout,
}
}
在 pages/profile.vue
中:
<template>
<div>
<h1>Welcome, {{ user?.name }}</h1>
<p>Theme: {{ theme }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
const theme = useState('theme', () => 'light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
const themeCookie = useCookie('theme')
watch(theme, (newTheme) => {
themeCookie.value = newTheme
})
onMounted(() => {
if (themeCookie.value) {
theme.value = themeCookie.value
}
})
</script>
在需要客戶端渲染的組件中使用 <ClientOnly>
包裹:
<template>
<div>
<h1>User Profile</h1>
<ClientOnly>
<pre>{{ userJson }}</pre>
<template #fallback>
<p>Loading user data...</p>
</template>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
const userJson = computed(() => JSON.stringify(user.value, null, 2))
</script>
Nuxt3 提供了幾個關鍵的生命週期鉤子:
useHead
: 用於管理頁面的 head 信息useFetch
: 用於在組件加載前獲取數據onMounted
: 在組件掛載後執行onUnmounted
: 在組件卸載前執行在 pages/users/[id].vue
中:
<script setup lang="ts">
const route = useRoute()
const userId = computed(() => route.params.id as string)
useHead({
title: computed(() => `User ${userId.value}`),
})
const { data: user, pending, error } = await useFetch(`/api/users/${userId.value}`)
onMounted(() => {
console.log('Component mounted')
})
onUnmounted(() => {
console.log('Component will unmount')
})
</script>
<template>
<div>
<h1>User Details</h1>
<p v-if="pending">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<div v-else>
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
</div>
</div>
</template>
在本文中,我們深入探討了 Nuxt3 中的路由管理和中間件的使用,並結合了 TypeScript、Pinia、等技術來構建一個強大而靈活的路由系統。我們還討論了如何處理 cookies、客戶端存儲和 hydration 問題,以及如何利用 Nuxt3 的生命週期鉤子來優化應用性能。
通過實施這些最佳實踐,開發者可以充分利用 Nuxt3 的強大功能,構建出類型安全、高效且易於維護的現代 Web 應用。同時,整合諸如 Pinia ,進一步增強了應用的可靠性和開發效率。
在處理客戶端特定的操作時,始終要考慮服務器端渲染(SSR)的環境,並適當使用 <ClientOnly>
組件和條件性的代碼執行來避免潛在的 hydration 問題。此外,利用 Nuxt3 提供的 useFetch
和 $fetch
方法進行數據獲取,可以確保更好的性能和一致性。
通過掌握這些概念和技術,你將能夠在 Nuxt3 中創建出富有表現力和高度互動的路由系統,為用戶提供流暢的導航體驗。